/*
 * $Id: inotify.c 520 2008-05-14 08:43:58Z jasta00 $
 *
 * Copyright (C) 2008 Josh Guilfoyle <jasta@devtcg.org>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * XXX: This code was borrowed from inotify-glib-0.0.3.
 */

#include "main.h"

#include <errno.h>

#include "inotify.h"

/*****************************************************************************/

#define INOTIFY_BUF 32768

struct callback
{
	MyInotifyCb cb;
	gpointer udata;
	GDestroyNotify notify;
};

struct wrapper
{
	GIOChannel *handle;
	WatchCreateDirCb create_dir;
	WatchMoveCb move;
	WatchModifyCb modify;
	WatchDeleteCb delete;

	struct
	{
		char *src;
		unsigned int cookie;
	} move_pair;
};

/* XXX: Managed by add and remove watch but exclusively used by the wrapper. */
static GHashTable *watches = NULL;

/*****************************************************************************/

GIOChannel *my_inotify_open(void)
{
	GIOChannel *gio;
	int fd;

	/* XXX: We don't [currently] support opening multiple inotify
	 * handles. */
	assert(watches == NULL);
	watches = g_hash_table_new_full(g_int_hash, g_int_equal, g_free, g_free);

	if ((fd = inotify_init()) < 0)
	{
		g_error("inotify_init(): %s", g_strerror(errno));
		return NULL;
	}

	gio = g_io_channel_unix_new(fd);
	assert(gio != NULL);

	return gio;
}

void my_inotify_close(GIOChannel *gio)
{
	assert(gio != NULL);

	/* XXX: Do we need to remove the watches first?  I assume not. */
	g_hash_table_destroy(watches);
	watches = NULL;

	g_io_channel_close(gio);
}

int my_inotify_add_watch(GIOChannel *gio, const char *path)
{
	int ifd;
	int wd;
	int mask = IN_MOVE | IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF;

	assert(gio != NULL);
	assert(path != NULL);

	ifd = g_io_channel_unix_get_fd(gio);

	if ((wd = inotify_add_watch(ifd, path, mask)) == -1)
	{
		g_warning("inotify_add_watch(%d, %s, %u): %s", ifd, path, mask,
		  g_strerror(errno));
		return -1;
	}

	g_hash_table_insert(watches, g_memdup(&wd, sizeof(wd)), g_strdup(path));

	return wd;
}

int my_inotify_remove_watch(GIOChannel *gio, int wd)
{
	assert(gio != NULL);

	if (g_hash_table_remove(watches, &wd) == FALSE)
		g_warning("removed unwatched wd=%d", wd);

	return inotify_rm_watch(g_io_channel_unix_get_fd(gio), wd);
}

struct watch_find
{
	GIOChannel *handle;
	const char *path;
	int found;
};

static gboolean find_watch_by_path(gpointer key, gpointer value, gpointer udata)
{
	struct watch_find *find = udata;

	if (strcmp(value, find->path) == 0)
	{
		find->found = *(int *)key;
		return TRUE;
	}

	return FALSE;
}

static int remove_watch_by_path(GIOChannel *gio, const char *path)
{
	struct watch_find find = { handle: gio, path: path, found: -1 };
	guint count;

	count = g_hash_table_foreach_remove(watches, find_watch_by_path, &find);
	assert(count <= 1);

	if (count == 0)
	{
		g_warning("not watching %s, so not removed...", path);
		return -1;
	}

	assert(find.found >= 0);
	return inotify_rm_watch(g_io_channel_unix_get_fd(gio), find.found);
}

static gboolean handle_event(GIOChannel *gio, GIOCondition cond, gpointer udata)
{
	char buf[INOTIFY_BUF];
	struct callback *cb = udata;
	GIOError err;
	guint len;
	int i = 0;

	err = g_io_channel_read(gio, buf, INOTIFY_BUF, &len);
	if (err != G_IO_ERROR_NONE)
	{
		g_warning("Error reading inotify: %s", g_strerror(errno));
		return FALSE;
	}

	while (i < len)
	{
		const char *name;
		struct inotify_event *ev;
		gboolean ret;

		assert(len - i >= sizeof(struct inotify_event));
		ev = (struct inotify_event *)&buf[i];

		if (ev->len > 0)
		{
			assert(len - i - sizeof(struct inotify_event) >= ev->len);
			name = &buf[i] + sizeof(struct inotify_event);
		}
		else
			name = NULL;

		ret = cb->cb(name, ev->wd, ev->mask, ev->cookie, cb->udata);

		if (ret == FALSE)
			return FALSE;

		i += sizeof(struct inotify_event) + ev->len;
	}

	return TRUE;
}

static void callback_free(gpointer udata)
{
	struct callback *wrap = udata;

	wrap->notify(wrap->udata);
	g_free(wrap);
}

void my_inotify_callback(GIOChannel *gio, MyInotifyCb cb, gpointer udata,
  GDestroyNotify notify)
{
	struct callback *wrap;

	assert(gio != NULL);
	assert(cb != NULL);

	wrap = g_new(struct callback, 1);
	wrap->cb = cb;
	wrap->udata = udata;
	wrap->notify = notify;

	g_io_add_watch_full(gio, G_PRIORITY_DEFAULT, G_IO_IN, handle_event, wrap,
	  callback_free);
}

/*****************************************************************************/

static void clear_move_pair(struct wrapper *wrap)
{
	assert(wrap != NULL);

	if (wrap->move_pair.src == NULL)
		return;

	g_free(wrap->move_pair.src);
	wrap->move_pair.src = NULL;

	wrap->move_pair.cookie = 0;
}

static void handle_move(const char *pathname, int wd,
  unsigned int event, unsigned int cookie, struct wrapper *wrap)
{
	assert((event & IN_MOVE) != 0);

	if (event & IN_MOVED_FROM)
	{
		wrap->move_pair.cookie = cookie;
		wrap->move_pair.src = g_strdup(pathname);
	}
	else if (event & IN_MOVED_TO)
	{
		if (wrap->move_pair.cookie == cookie &&
		    wrap->move_pair.src != NULL)
		{
			if (event & IN_ISDIR)
			{
				/* Hmm, can we actually just updated `watches' and move on?
				 * Does the watch internally understand moves? */
				remove_watch_by_path(wrap->handle, wrap->move_pair.src);
				my_inotify_add_watch(wrap->handle, pathname);
			}
			else
				wrap->move(wrap->move_pair.src, pathname);
		}
		else
		{
			if (event & IN_ISDIR)
				wrap->create_dir(pathname);
			else
				wrap->modify(pathname);
		}

		clear_move_pair(wrap);
	}
}

static gboolean wrapper_cb(const char *name, int wd, unsigned int event,
  unsigned int cookie, gpointer udata)
{
	char *full;
	char *path;
	struct wrapper *wrap = udata;

	path = g_hash_table_lookup(watches, &wd);
	//g_print("wd=%d: %s;%s (0x%08x, %u)\n", wd, path, name, event, cookie);

	/* Check if the previous event was IN_MOVED_FROM. */
	if (wrap->move_pair.src != NULL)
	{
		/* Try to match IN_MOVED_FROM to IN_MOVED_TO. */
		if ((event & IN_MOVED_TO) == 0)
		{
			if ((event & IN_ISDIR) == 0)
				wrap->delete(wrap->move_pair.src);

			clear_move_pair(wrap);
		}
		else
		{
			/* I dunno, the inotify interface makes me nervous... */
			assert((event & IN_IGNORED) == 0);
			assert((event & IN_DELETE_SELF) == 0);
		}
	}

	/* Why is this triggered?  Seems to follow IN_DELETE_SELF... */
	if (event & IN_IGNORED)
		return TRUE;

	/* Tidy up... */
	if (event & IN_DELETE_SELF)
	{
		my_inotify_remove_watch(wrap->handle, wd);
		return TRUE;
	}

	path = g_hash_table_lookup(watches, &wd);
	assert(path != NULL);

	full = g_strdup_printf("%s/%s", path, name);

	if (event & IN_MOVE)
		handle_move(full, wd, event, cookie, wrap);
	else if (event & IN_DELETE)
	{
		if ((event & IN_ISDIR) == 0)
			wrap->delete(full);
	}
	else if (event & IN_CREATE)
	{
		if (event & IN_ISDIR)
			my_inotify_add_watch(wrap->handle, full);
	}
	else if (event & IN_CLOSE_WRITE)
	{
		if ((event & IN_ISDIR) == 0)
			wrap->modify(full);
	}

	g_free(full);

	return TRUE;
}

static void wrapper_free(gpointer udata)
{
	struct wrapper *wrap = udata;

	if (wrap->move_pair.src != NULL)
		g_free(wrap->move_pair.src);

	g_free(wrap);
}

void my_inotify_wrapper(GIOChannel *gio, WatchCreateDirCb create_dir,
  WatchMoveCb move, WatchModifyCb modify, WatchDeleteCb delete)
{
	struct wrapper *wrap;

	assert(gio != NULL);

	wrap = g_new(struct wrapper, 1);
	wrap->handle = gio;
	wrap->create_dir = create_dir;
	wrap->move = move;
	wrap->modify = modify;
	wrap->delete = delete;
	wrap->move_pair.cookie = 0;
	wrap->move_pair.src = NULL;

	my_inotify_callback(gio, wrapper_cb, wrap, wrapper_free);
}
